'
' *  Authors:  Benton Stark
' * 
' *  Copyright (c) 2007-2012 Starksoft, LLC (http://www.starksoft.com) 
' *
' * Permission is hereby granted, free of charge, to any person obtaining a copy
' * of this software and associated documentation files (the "Software"), to deal
' * in the Software without restriction, including without limitation the rights
' * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
' * copies of the Software, and to permit persons to whom the Software is
' * furnished to do so, subject to the following conditions:
' * 
' * The above copyright notice and this permission notice shall be included in
' * all copies or substantial portions of the Software.
' * 
' * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
' * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
' * THE SOFTWARE.
' * 
' 


Imports System.Text.RegularExpressions
Imports System.Globalization
Imports System.Text

Namespace FTP
    ''' <summary>
    ''' Generic ftp file and directory MLSx listing parser that supports most Unix, Dos, and Windows style FTP 
    ''' directory listings.  A custom parser can be created using the IFtpItemParser interface in the event
    ''' this parser does not suit the needs of a specific FTP server directory format listing.
    ''' </summary>
    Public Class FtpMlsxItemParser
        Implements IFtpItemParser
        ' general information
        Private _name As String
        Private _rawText As String

        ' RFC defined MLSx object facts
        Private _modified As System.Nullable(Of DateTime)
        Private _size As Long
        Private _itemType As FtpItemType
        Private _create As System.Nullable(Of DateTime)
        ' create=
        Private _unique As String
        ' unique=
        Private _perm As MlsxPerm
        ' perm=
        Private _lang As String
        ' lang=
        Private _mediaType As String
        ' media-type=
        Private _charSet As String
        ' charset=
        ' UNIX specific MLSx object facts
        Private _unixGroup As System.Nullable(Of Int32)
        ' UNIX.group=
        Private _unixMode As System.Nullable(Of Int32)
        ' UNIX.mode=
        Private _unixOwner As System.Nullable(Of Int32)
        ' UNIX.owner=
        Private _attributes As String

        ' MLSx fact names
        Private Const FACT_SIZE As String = "size"
        Private Const FACT_MODIFY As String = "modify"
        Private Const FACT_CREATE As String = "create"
        Private Const FACT_TYPE As String = "type"
        Private Const FACT_UNIQUE As String = "unique"
        Private Const FACT_PERM As String = "perm"
        Private Const FACT_LANG As String = "lang"
        Private Const FACT_MEDIA_TYPE As String = "media-type"
        Private Const FACT_CHAR_SET As String = "charset"

        ' MLSx extension fact names
        Private Const FACT_EXT_UNIX_GROUP As String = "unix.group"
        Private Const FACT_EXT_UNIX_MODE As String = "unix.mode"
        Private Const FACT_EXT_UNIX_OWNER As String = "unix.owner"

        ' MLSx media type indicators
        Private Const TYPE_FILE As String = "file"
        Private Const TYPE_CURRENT_DIRECTORY As String = "cdir"
        Private Const TYPE_DIRECTORY As String = "dir"
        Private Const TYPE_PARENT_DIRECTORY As String = "pdir"

        ' MLSx permission indicators
        Private Const PERM_CAN_APPENDED_FILE As Char = "a"c
        Private Const PERM_CAN_CREATE_FILE As Char = "c"c
        Private Const PERM_CAN_DELETE_FILE As Char = "d"c
        Private Const PERM_CAN_CHANGE_DIRECTORY As Char = "e"c
        Private Const PERM_CAN_RENAME As Char = "f"c
        Private Const PERM_CAN_LIST_FILES As Char = "l"c
        Private Const PERM_CAN_CREATE_DIRECTORY As Char = "m"c
        Private Const PERM_CAN_DELETE_DIRECTORY As Char = "p"c
        Private Const PERM_CAN_RETRIEVE_FILE As Char = "r"c
        Private Const PERM_CAN_STORE_FILE As Char = "w"c

        ' MLSx date/time field size
        Private Const DATE_TIME_MIN_LEN As Integer = 14
        Private Const DATE_TIME_MAX_LEN As Integer = 18

        ''' <summary>
        ''' Method to parse a line of file listing data from the FTP server.
        ''' </summary>
        ''' <param name="line">Line to parse.</param>
        ''' <return>Object representing data in parsed file listing line.</return>
        Public Function ParseLine(line As String) As FtpItem Implements IFtpItemParser.ParseLine
            If [String].IsNullOrEmpty(line) Then
                Throw New FtpItemParsingException("line cannot be empty")
            End If
            Parse(line)

            Dim m As New FtpMlsxItem(_name, _modified, _size, _itemType, _attributes, _unixMode, _
                _lang, _create, _unique, _perm, _lang, _mediaType, _
                _charSet, _unixGroup, _unixOwner)

            Return m
        End Function

        Private Sub Parse(line As String)
            _rawText = line

            ' ex: Type=file;Size=1830;Modify=19940916055648;Perm=r; hatch.c
            ' ex: modify=20120822211414;perm=adfr;size=2101;type=file;unique=16UF3F5;UNIX.group=49440;UNIX.mode=0744;UNIX.owner=49440; iphone_settings_icon.jpg
            ' ex: modify=20030225143801;perm=adfr;size=503;type=file;unique=12U24470006;UNIX.group=0;UNIX.mode=0644;UNIX.owner=0; welcome.msg
            Dim fields As String() = line.Split(New Char() {";"c}, StringSplitOptions.RemoveEmptyEntries)

            If fields.Length = 0 Then
                Return
            End If

            ' parse fact fields
            For i As Integer = 0 To fields.Length - 2
                ParseField(fields(i))
            Next

            ' last field is always name of the object
            ParseName(fields(fields.Length - 1))

            If _name = "." Then
                _itemType = FtpItemType.CurrentDirectory
            End If
            If _name = ".." Then
                _itemType = FtpItemType.ParentDirectory
            End If
        End Sub

        Private Sub ParseName(name As String)
            If [String].IsNullOrEmpty(name) Then
                Throw New FtpItemParsingException("name cannot be a null or empty value")
            End If

            If name.Length < 2 Then
                Throw New FtpItemParsingException("name length is too short")
            End If

            ' first character must always be a space
            If Not (name(0) = " "c AndAlso name(1) <> " "c) Then
                Throw New FtpItemParsingException("name is not in proper format")
            End If

            _name = name.Trim()
        End Sub

        Private Sub ParseField(field As String)
            If [String].IsNullOrEmpty(field) Then
                Throw New FtpItemParsingException("field cannot be a null or empty value")
            End If

            If field.IndexOf("="c) = -1 Then
                Throw New FtpItemParsingException("field must contain equals '=' value")
            End If

            ' split the field fact
            Dim fact As String() = field.Split("="c)

            If fact.Length <> 2 Then
                Throw New FtpItemParsingException("field must contain equals '=' value with only a name and value")
            End If

            Dim name As String = fact(0).Trim().ToLower()
            Dim value As String = fact(1).Trim()

            If name.Length = 0 Then
                Throw New FtpItemParsingException("field fact name cannot be empty")
            End If

            Select Case name
                Case FACT_CHAR_SET
                    _charSet = value
                    Exit Select
                Case FACT_CREATE
                    _create = ParseDateTime(value)
                    Exit Select
                Case FACT_LANG
                    _lang = value
                    Exit Select
                Case FACT_MEDIA_TYPE
                    _mediaType = value
                    Exit Select
                Case FACT_MODIFY
                    _modified = ParseDateTime(value)
                    Exit Select
                Case FACT_PERM
                    _perm = ParsePerm(value)
                    Exit Select
                Case FACT_SIZE
                    _size = ParseSize(value)
                    Exit Select
                Case FACT_TYPE
                    _itemType = ParseType(value)
                    Exit Select
                Case FACT_UNIQUE
                    _unique = value
                    Exit Select
                Case FACT_EXT_UNIX_GROUP
                    _unixGroup = ParseInt32(value)
                    Exit Select
                Case FACT_EXT_UNIX_MODE
                    _unixMode = ParseInt32(value)
                    If _unixMode IsNot Nothing Then
                        _attributes = FtpUtilities.ModeToAttribute(_unixMode.Value)
                    End If
                    Exit Select
                Case FACT_EXT_UNIX_OWNER
                    _unixOwner = ParseInt32(value)
                    Exit Select
                Case Else
                    Exit Select
            End Select
        End Sub

        Private Function ParseDateTime(v As String) As System.Nullable(Of DateTime)
            If [String].IsNullOrEmpty(v) Then
                Return Nothing
            End If

            If v.Length < DATE_TIME_MIN_LEN Then
                Throw New FtpItemParsingException([String].Format("MLSx '{0}' date/time value too small", v))
            End If

            If v.Length > DATE_TIME_MAX_LEN Then
                Throw New FtpItemParsingException([String].Format("MLSx '{0}' date/time value too large", v))
            End If

            ' YYYYMMDDHHMMSS.sss
            Dim yr As Integer = Integer.Parse(v.Substring(0, 4), CultureInfo.InvariantCulture)
            Dim mon As Integer = Integer.Parse(v.Substring(4, 2), CultureInfo.InvariantCulture)
            Dim day As Integer = Integer.Parse(v.Substring(6, 2), CultureInfo.InvariantCulture)
            Dim hr As Integer = Integer.Parse(v.Substring(8, 2), CultureInfo.InvariantCulture)
            Dim min As Integer = Integer.Parse(v.Substring(10, 2), CultureInfo.InvariantCulture)
            Dim sec As Integer = Integer.Parse(v.Substring(12, 2), CultureInfo.InvariantCulture)
            Dim ms As Integer = 0
            ' optional
            If v.Length = DATE_TIME_MAX_LEN Then
                ms = Integer.Parse(v.Substring(15, 3), CultureInfo.InvariantCulture)
            End If

            Dim dateUtc As New DateTime(yr, mon, day, hr, min, sec, _
                ms, DateTimeKind.Utc)

            ' adjust the UTC (GMT) time to local time
            Return New DateTime(dateUtc.ToLocalTime().Ticks)
        End Function

        Private Function ParseInt32(v As String) As System.Nullable(Of Int32)
            If [String].IsNullOrEmpty(v) Then
                Return Nothing
            End If

            Dim r As Integer
            If Not Int32.TryParse(v, r) Then
                Throw New FtpItemParsingException("MLSx fact value cannot be converted to integer value")
            End If

            Return r
        End Function

        Private Function ParseSize(v As String) As Long
            If [String].IsNullOrEmpty(v) Then
                Return 0
            End If

            Dim r As Long
            If Not Int64.TryParse(v, r) Then
                Throw New FtpItemParsingException("MLSx 'size' fact value cannot be converted to integer value")
            End If

            Return r
        End Function

        Private Function ParsePerm(v As String) As MlsxPerm
            If [String].IsNullOrEmpty(v) Then
                Return MlsxPerm.None
            End If

            Dim pary As Char() = v.ToLower().ToCharArray()

            Dim pem As MlsxPerm = MlsxPerm.None

            For Each pchar As Char In pary
                Select Case pchar
                    Case PERM_CAN_APPENDED_FILE
                        pem = pem Or MlsxPerm.CanAppendFile
                        Exit Select
                    Case PERM_CAN_CHANGE_DIRECTORY
                        pem = pem Or MlsxPerm.CanChangeDirectory
                        Exit Select
                    Case PERM_CAN_CREATE_DIRECTORY
                        pem = pem Or MlsxPerm.CanCreateDirectory
                        Exit Select
                    Case PERM_CAN_CREATE_FILE
                        pem = pem Or MlsxPerm.CanCreateFile
                        Exit Select
                    Case PERM_CAN_DELETE_DIRECTORY
                        pem = pem Or MlsxPerm.CanDeleteDirectory
                        Exit Select
                    Case PERM_CAN_DELETE_FILE
                        pem = pem Or MlsxPerm.CanDeleteFile
                        Exit Select
                    Case PERM_CAN_LIST_FILES
                        pem = pem Or MlsxPerm.CanListFiles
                        Exit Select
                    Case PERM_CAN_RENAME
                        pem = pem Or MlsxPerm.CanRename
                        Exit Select
                    Case PERM_CAN_RETRIEVE_FILE
                        pem = pem Or MlsxPerm.CanRetrieveFile
                        Exit Select
                    Case PERM_CAN_STORE_FILE
                        pem = pem Or MlsxPerm.CanStoreFile
                        Exit Select
                    Case Else
                        Throw New FtpItemParsingException([String].Format("unknown MLSx 'perm' value '{0}' encountered", pchar))
                End Select
            Next

            Return pem
        End Function

        Private Function ParseType(v As String) As FtpItemType
            If [String].IsNullOrEmpty(v) Then
                Return FtpItemType.None
            End If

            Select Case v.ToLower()
                Case TYPE_CURRENT_DIRECTORY
                    Return FtpItemType.CurrentDirectory
                Case TYPE_DIRECTORY
                    Return FtpItemType.Directory
                Case TYPE_FILE
                    Return FtpItemType.File
                Case TYPE_PARENT_DIRECTORY
                    Return FtpItemType.ParentDirectory
                Case Else
                    Throw New FtpItemParsingException([String].Format("unknown MLSx 'type' value '{0}' encountered", v))
            End Select
        End Function


    End Class
End Namespace
